# 【按键精灵】69. 免费调用Yolo识别(V2.0) - 自动监控目录识别（https://www.bilibili.com/video/BV1iT421k7YR/）
# YOLOv5 🚀 by Ultralytics, GPL-3.0 license
"""
Run YOLOv5 detection inference on images, videos, directories, globs, YouTube, webcam, streams, etc.

Usage - sources:
    $ python detect.py --weights yolov5s.pt --source 0                               # webcam
                                                     img.jpg                         # image
                                                     vid.mp4                         # video
                                                     screen                          # screenshot
                                                     path/                           # directory
                                                     'path/*.jpg'                    # glob
                                                     'https://youtu.be/Zgi9g1ksQHc'  # YouTube
                                                     'rtsp://example.com/media.mp4'  # RTSP, RTMP, HTTP stream

Usage - formats:
    $ python detect.py --weights yolov5s.pt                 # PyTorch
                                 yolov5s.torchscript        # TorchScript
                                 yolov5s.onnx               # ONNX Runtime or OpenCV DNN with --dnn
                                 yolov5s_openvino_model     # OpenVINO
                                 yolov5s.engine             # TensorRT
                                 yolov5s.mlmodel            # CoreML (macOS-only)
                                 yolov5s_saved_model        # TensorFlow SavedModel
                                 yolov5s.pb                 # TensorFlow GraphDef
                                 yolov5s.tflite             # TensorFlow Lite
                                 yolov5s_edgetpu.tflite     # TensorFlow Edge TPU
                                 yolov5s_paddle_model       # PaddlePaddle
"""

import argparse
import os
import sys
from pathlib import Path

import torch

FILE = Path(__file__).resolve()
ROOT = FILE.parents[0]  # YOLOv5 root directory
if str(ROOT) not in sys.path:
    sys.path.append(str(ROOT))  # add ROOT to PATH
ROOT = Path(os.path.relpath(ROOT, Path.cwd()))  # relative

from models.common import DetectMultiBackend
from utils.dataloaders import IMG_FORMATS, VID_FORMATS, LoadImages, LoadScreenshots, LoadStreams
from utils.general import (LOGGER, Profile, check_file, check_img_size, check_requirements, non_max_suppression, scale_boxes, strip_optimizer, xyxy2xywh)
from utils.torch_utils import select_device, smart_inference_mode

# 导入watchdog相关库
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

@smart_inference_mode()
def run(
        model,
        weights=ROOT / 'yolov5s.pt',  # model path or triton URL
        source=ROOT / 'data/images',  # file/dir/URL/glob/screen/0(webcam)
        imgsz=(640, 640),  # inference size (height, width)
        conf_thres=0.25,  # confidence threshold
        iou_thres=0.45,  # NMS IOU threshold
        max_det=1000,  # maximum detections per image
        classes=None,  # filter by class: --class 0, or --class 0 2 3
        agnostic_nms=False,  # class-agnostic NMS
        augment=False,  # augmented inference
        update=False,  # update all models
        vid_stride=1,  # video frame-rate stride
):
    source = str(source)
    is_file = Path(source).suffix[1:] in (IMG_FORMATS + VID_FORMATS)
    is_url = source.lower().startswith(('rtsp://', 'rtmp://', 'http://', 'https://'))
    webcam = source.isnumeric() or source.endswith('.txt') or (is_url and not is_file)
    screenshot = source.lower().startswith('screen')
    if is_url and is_file:
        source = check_file(source)  # download

    # Load model（原加载模型代码，现加载模型已前置）
    stride, names, pt = model.stride, model.names, model.pt
    imgsz = check_img_size(imgsz, s=stride)  # check image size

    # Dataloader
    bs = 1  # batch_size
    if webcam:
        dataset = LoadStreams(source, img_size=imgsz, stride=stride, auto=pt, vid_stride=vid_stride)
        bs = len(dataset)
    elif screenshot:
        dataset = LoadScreenshots(source, img_size=imgsz, stride=stride, auto=pt)
    else:
        dataset = LoadImages(source, img_size=imgsz, stride=stride, auto=pt, vid_stride=vid_stride)

    # Run inference
    model.warmup(imgsz=(1 if pt or model.triton else bs, 3, *imgsz))  # warmup
    seen, windows, dt = 0, [], (Profile(), Profile(), Profile())
    for path, im, im0s, vid_cap, s in dataset:
        with dt[0]:
            im = torch.from_numpy(im).to(model.device)
            im = im.half() if model.fp16 else im.float()  # uint8 to fp16/32
            im /= 255  # 0 - 255 to 0.0 - 1.0
            if len(im.shape) == 3:
                im = im[None]  # expand for batch dim

        # Inference
        with dt[1]:
            pred = model(im, augment=augment, visualize=False)

        # NMS
        with dt[2]:
            pred = non_max_suppression(pred, conf_thres, iou_thres, classes, agnostic_nms, max_det=max_det)

        # Second-stage classifier (optional)
        # pred = utils.general.apply_classifier(pred, classifier_model, im, im0s)

        # Process predictions
        for i, det in enumerate(pred):  # per image
            seen += 1
            if webcam:  # batch_size >= 1
                p, im0, frame = path[i], im0s[i].copy(), dataset.count
                s += f'{i}: '
            else:
                p, im0, frame = path, im0s.copy(), getattr(dataset, 'frame', 0)

            p = Path(p)  # to Path
            s += '%gx%g ' % im.shape[2:]  # print string
            gn = torch.tensor(im0.shape)[[1, 0, 1, 0]]  # normalization gain whwh
            if len(det):
                # Rescale boxes from img_size to im0 size
                det[:, :4] = scale_boxes(im.shape[2:], det[:, :4], im0.shape).round()

                # Print results
                for c in det[:, 5].unique():
                    n = (det[:, 5] == c).sum()  # detections per class
                    s += f"{n} {names[int(c)]}{'s' * (n > 1)}, "  # add to string

                # Write results
                filename = path.split('\\')[-1] # 获取文件名
                results = f'{filename}:' # 每一张图片的识别结果
                for *xyxy, conf, cls in reversed(det):
                    c = int(cls.item())  # 使用item()方法获取张量的值，并转换为int
                    xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist()
                    x_center, y_center, width, height = xywh # 这里的坐标和宽高不是像素值，要换算
                    H, W, channels = im0s.shape # 原始图像高度宽度
                    x_pixel = round(x_center * W) # 目标中心点X像素坐标
                    y_pixel = round(y_center * H) # 目标中心点Y像素坐标
                    width_pixel = round(width * W) # 目标像素宽度
                    height_pixel = round(height * H) # 目标像素高度
                    x_min_pixel = x_pixel - width_pixel / 2 # 目标左上角X像素坐标
                    y_min_pixel = y_pixel - height_pixel / 2 # 目标左上角Y像素坐标
                    result = f"{names[c]},{conf:.2f},{x_pixel},{y_pixel},{width_pixel},{height_pixel}"
                    # print(result)
                    results += f"|{result}" if results != f'{filename}:' else result
                print(results, flush=True) # 输出给按键精灵并立即刷新缓冲区，多张图片就有多行输出（没有识别到目标的除外）
            print('over', flush=True)

    if update:
        strip_optimizer(weights[0])  # update model (to fix SourceChangeWarning)

# 定义 ImageHandler 类，继承自 FileSystemEventHandler，它会在检测到指定目录下有新文件创建时调用 on_created 方法。在该方法中，我们。
class ImageHandler(FileSystemEventHandler):
    def __init__(self, model, args):
        # self.model = model
        self.args = args
        self.args.model = model
    # 此方法调用 run 函数对新图片进行识别
    def on_created(self, event):
        # 当文件被创建时触发
        if event.src_path.endswith(tuple(IMG_FORMATS)):  # 确保处理的是图片格式
            # print(f"New image detected: {event.src_path}")
            # 把source设置为具体图片路径，并执行 run 进行识别
            self.args.source = event.src_path
            run(**vars(self.args))

# 初始化模型，并设置一个观察者(Observer)来监控指定目录。一旦有新图片被创建，观察者就会触发事件，进而调用识别逻辑。
def monitor_and_detect(**kwargs):
    # 监控指定目录并识别新图片
    # 定义 DetectMultiBackend 所需的参数列表，从 kwargs 中筛选出必要的参数
    required_keys = ['weights', 'dnn', 'data', 'fp16']
    model_kwargs = {k: v for k, v in kwargs.items() if k in required_keys}
    source=Path(kwargs.get('source', ''))
    model = DetectMultiBackend(weights=model_kwargs.get('weights'), device=select_device(kwargs.get('device', '')), dnn=model_kwargs.get('dnn', False), 
                              data=model_kwargs.get('data', ''), fp16=model_kwargs.get('fp16', False))  # 加载模型
    
    # run 函数需要传递的参数
    required_keys = ['model', 'weights', 'source', 'imgsz', 'conf_thres', 'iou_thres', 'max_det', 'classes', 'agnostic_nms', 'augment', 'update', 'vid_stride']
    namespace_kwargs = {k: v for k, v in kwargs.items() if k in required_keys}
    args = argparse.Namespace(**namespace_kwargs)  # 创建命名空间对象用于传递参数

    event_handler = ImageHandler(model=model, args=args)
    observer = Observer()
    observer.schedule(event_handler, str(source), recursive=False)  # 非递归监控
    observer.start()
    print('ready', flush=True)
    try:
        while True:
            pass  # 保持主程序运行，让Observer线程持续工作
    except KeyboardInterrupt:
        observer.stop()
    observer.join()

def parse_opt():
    parser = argparse.ArgumentParser()
    parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'yolov5s.pt', help='model path or triton URL')
    parser.add_argument('--source', type=str, default=ROOT / 'data/images', help='file/dir/URL/glob/screen/0(webcam)')
    parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='(optional) dataset.yaml path')
    parser.add_argument('--imgsz', '--img', '--img-size', nargs='+', type=int, default=[640], help='inference size h,w')
    parser.add_argument('--conf-thres', type=float, default=0.25, help='confidence threshold') # 置信度
    parser.add_argument('--iou-thres', type=float, default=0.45, help='NMS IoU threshold') # IOU
    parser.add_argument('--max-det', type=int, default=1000, help='maximum detections per image')
    parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
    parser.add_argument('--classes', nargs='+', type=int, help='filter by class: --classes 0, or --classes 0 2 3')
    parser.add_argument('--agnostic-nms', action='store_true', help='class-agnostic NMS')
    parser.add_argument('--augment', action='store_true', help='augmented inference')
    parser.add_argument('--update', action='store_true', help='update all models')
    parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference')
    parser.add_argument('--dnn', action='store_true', help='use OpenCV DNN for ONNX inference')
    parser.add_argument('--vid-stride', type=int, default=1, help='video frame-rate stride')
    opt = parser.parse_args()
    opt.imgsz *= 2 if len(opt.imgsz) == 1 else 1  # expand
    # print_args(vars(opt))
    return opt


def main(opt):
    check_requirements(exclude=('tensorboard', 'thop'))
    run(**vars(opt))


if __name__ == "__main__":
    opt = parse_opt()
    # main(opt)
    # 修改主函数调用，使用监控并识别逻辑
    monitor_and_detect(**vars(opt))
